/*
 * Copyright (C) 2013, Pavel Samko <bulldozerbsg@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the "Pavel Samko" nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

#include "vfsvirtfs.h"
#include "vfslib.h"
#include "vfsio.h"
#include "vfsmod.h"
#include "vfscore.h"
#include "vfsformat.h"
#include "vfscrc16.h"
#include "vfscrypttea.h"
#include "vfspacklzma.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>

#define VIRTFS_PROBE_DATA_COUNT 256
#define VIRTFS_PROBE_DATA_SIZE 256
#define VIRTFS_FILES_COUNT VIRTFS_PROBE_DATA_COUNT

#define VIRTFS_MIN_SUPPORTED_VER 1
#define VIRTFS_MAX_SUPPORTED_VER 1

char g_virtfs_data[VIRTFS_PROBE_DATA_COUNT][VIRTFS_PROBE_DATA_SIZE];
size_t g_virtfs_data_count;

char g_virtfs_magic[] = VIRTFS_MAGIC_DATA;

MOD g_virtfs_mod =
{
	"Virt FS Mod",
	vfs_virtfs_fclose,
	NULL,
	vfs_virtfs_fopen,
	vfs_virtfs_fread,
	vfs_virtfs_fseek,
	vfs_virtfs_ftell,
	NULL,
	NULL,
	NULL,
	0
};

int vfs_virtfs_probe_vfs(VFILE * f, VFS_PARAMS * params, size_t params_count)
{
	int result = 0;
	MOD mod;
	VIRTFS_HEADER header;
	if (f && (g_virtfs_data_count < VIRTFS_PROBE_DATA_COUNT))
	{
		vfs_fseek(f, 0, SEEK_SET);
		if (vfs_fread(&header, sizeof(VIRTFS_HEADER), 1, f) && !memcmp(&header.m_magic, g_virtfs_magic, sizeof(VIRTFS_MAGIC)) && (header.m_version >= VIRTFS_MIN_SUPPORTED_VER) && (header.m_version <= VIRTFS_MAX_SUPPORTED_VER))
		{
			strcpy(g_virtfs_data[g_virtfs_data_count], f->m_name);
			memcpy(&mod, &g_virtfs_mod, sizeof(MOD));
			mod.m_data = (void *)g_virtfs_data[g_virtfs_data_count];
			mod.m_params = params;
			mod.m_params_count = params_count;
			++g_virtfs_data_count;
			result = vfs_core_add_mod(&mod);
		}
	}
	return result;
}

typedef struct VIRTFSFILE
{
	VFILE * m_file;
	uint32_t m_ref_count;
	char * m_file_name; // ref to g_virtfs_data[]
} VIRTFSFILE;

VIRTFSFILE g_virtfs_files[VIRTFS_FILES_COUNT];

typedef struct VDATA
{
	VIRTFSFILE * m_rec;
	VIRTFS_RECORD m_filerec;
	int32_t m_offset; // internal file
	MOD * m_mod;
	VIRTFS_PACKTABLE m_packtable;
	VIRTFS_PACKTABLE_RECORD * m_packtable_recs;
	// cache for lzma
	size_t m_pack_offset;
	void * m_pack_buff;
} VDATA;

int vfs_virtfs_cheakmode(const char * mode)
{
	return !strcmp(mode, "rb") || !strcmp(mode, "br");
}

void vfs_virtfs_clearrec(VIRTFSFILE * filerec)
{
	if (filerec)
	{
		memset(filerec, 0, sizeof(VIRTFSFILE));
	}
}

VIRTFSFILE * vfs_virtfs_findzerorec(void)
{
	size_t i;
	for (i = 0; i < VIRTFS_FILES_COUNT; ++i)
	{
		if (!g_virtfs_files[i].m_ref_count)
		{
			return &g_virtfs_files[i];
		}
	}
	return NULL;
}

VIRTFSFILE * vfs_virtfs_findrec(const char * name)
{
	size_t i;
	if (name && strlen(name))
	{
		for (i = 0; i < VIRTFS_FILES_COUNT; ++i)
		{
			if (g_virtfs_files[i].m_ref_count && g_virtfs_files[i].m_file && g_virtfs_files[i].m_file_name && !strcmp(g_virtfs_files[i].m_file_name, name))
			{
				return &g_virtfs_files[i];
			}
		}
	}
	return NULL;
}

int vfs_virtfs_findfilerec(const char * name, VFILE * f, VIRTFS_RECORD * filerec)
{
	VIRTFS_HEADER header;
	VIRTFS_NAMEHASH namehash;
	uint16_t name_crc16_ccitt;
	size_t name_size;
	size_t i;
	if (name && f && filerec && (name_size = strlen(name)))
	{
		memset(filerec, 0, sizeof(VIRTFS_RECORD));
		vfs_fseek(f, 0, SEEK_SET);
		if (vfs_fread(&header, sizeof(VIRTFS_HEADER), 1, f) && !memcmp(&header.m_magic, g_virtfs_magic, sizeof(VIRTFS_MAGIC)) && (header.m_version >= VIRTFS_MIN_SUPPORTED_VER) && (header.m_version <= VIRTFS_MAX_SUPPORTED_VER))
		{
			if (header.m_name_hash_table_offset)
			{
				//has name crc's
				name_crc16_ccitt = vfs_crc16_ccitt(VFS_CRC16_CCITT_INIT, (unsigned char *)name, name_size);
				for (i = 0; i < header.m_rec_count; ++i)
				{
					vfs_fseek(f, i * sizeof(VIRTFS_NAMEHASH) + header.m_name_hash_table_offset, SEEK_SET);
					if (vfs_fread(&namehash, sizeof(VIRTFS_NAMEHASH), 1, f) && (namehash.m_crc16_ccitt == name_crc16_ccitt))
					{
						vfs_fseek(f, namehash.m_offset, SEEK_SET);
						vfs_fread(filerec, sizeof(VIRTFS_RECORD) - VIRTFS_NAME_SIZE, 1, f);
						vfs_fread(filerec->m_name, filerec->m_name_size, 1, f);
						if ((name_size == filerec->m_name_size) && !memcmp(name, filerec->m_name, name_size))
						{
							return 1;
						}
					}
				}
			}
			vfs_fseek(f, sizeof(VIRTFS_HEADER), SEEK_SET);
			for (i = 0; i < header.m_rec_count; ++i)
			{
				vfs_fread(filerec, sizeof(VIRTFS_RECORD) - VIRTFS_NAME_SIZE, 1, f);
				vfs_fread(filerec->m_name, filerec->m_name_size, 1, f);
				if ((name_size == filerec->m_name_size) && !memcmp(name, filerec->m_name, name_size))
				{
					return 1;
				}
			}
		}
	}
	return 0;
}

VDATA * vfs_virtfs_openfile(const char * name, VIRTFSFILE * rec)
{
	VDATA * vdata = NULL;
	VIRTFS_RECORD filerec;
	VFILE * f;
	if (name && rec && (f = rec->m_file))
	{
		if (vfs_virtfs_findfilerec(name, f, &filerec) && (vdata = (VDATA *)malloc(sizeof(VDATA))))
		{
			memset(vdata, 0, sizeof(VDATA)); // set offset 0
			vdata->m_rec = rec;
			memcpy(&vdata->m_filerec, &filerec, sizeof(VIRTFS_RECORD));
			vdata->m_rec->m_ref_count++;
		}
	}
	return vdata;
}

int vfs_virtfs_fclose(void * mod_data)
{
	VDATA * vdata = (VDATA *)mod_data;
	if (vdata && vdata->m_rec)
	{
		vdata->m_rec->m_ref_count--;
		if (!vdata->m_rec->m_ref_count)
		{
			vfs_fclose(vdata->m_rec->m_file);
			vfs_virtfs_clearrec(vdata->m_rec);
			if (vdata->m_packtable_recs)
			{
				free(vdata->m_packtable_recs);
			}
			if (vdata->m_pack_buff)
			{
				free(vdata->m_pack_buff);
			}
		}
		free(vdata);
		return 0;
	}
	return EOF;
}

void * vfs_virtfs_fopen(const char * name, const char * mode, MOD * mod)
{
	VDATA * vdata = NULL;
	VIRTFSFILE * rec;
	if (name && vfs_virtfs_cheakmode(mode) && mod && mod->m_data && strlen((const char *)mod->m_data))
	{
		rec = vfs_virtfs_findrec((const char *)mod->m_data);
		if (!rec && (rec = vfs_virtfs_findzerorec()))
		{
			if (!(rec->m_file = vfs_fopen((const char *)mod->m_data, "rb")))
			{
				rec = NULL;
			} else
			{
				rec->m_file_name = (char *)mod->m_data;
			}
		}
		if (rec)
		{
			vdata = vfs_virtfs_openfile(name, rec);
			if (vdata)
			{
				vdata->m_mod = mod;
				if (vdata->m_filerec.m_compress_format != VIRTFS_COMPRESS_NONE)
				{
					vfs_fseek(vdata->m_rec->m_file, vdata->m_filerec.m_offset, SEEK_SET);
					vfs_fread(&vdata->m_packtable, sizeof(VIRTFS_PACKTABLE), 1, vdata->m_rec->m_file);
					if (vdata->m_packtable.m_rec_count && (vdata->m_packtable_recs = (VIRTFS_PACKTABLE_RECORD *)malloc(vdata->m_packtable.m_rec_count * sizeof(VIRTFS_PACKTABLE_RECORD))))
					{
						vfs_fread(vdata->m_packtable_recs, sizeof(VIRTFS_PACKTABLE_RECORD), vdata->m_packtable.m_rec_count , vdata->m_rec->m_file);
					}
				}
				if (vdata->m_filerec.m_compress_format == VIRTFS_COMPRESS_LZMA)
				{
					vdata->m_pack_buff = malloc(VFS_LZMA_BLOCKSIZE);
				}
			}
			if (!vdata && !rec->m_ref_count)
			{
				vfs_fclose(rec->m_file);
				vfs_virtfs_clearrec(rec);
			}
		}
	}
	return (void *)vdata;
}

uint32_t * vfs_virtfs_getkey(MOD * mod)
{
	size_t i;
	if (mod)
	{
		for (i = 0; i < mod->m_params_count; ++i)
		{
			if ((mod->m_params + i)->m_key  == VFS_PARAM_TEAKEY)
			{
				return (uint32_t *)(mod->m_params + i)->m_data;
			}
		}
	}
	return NULL;
}

size_t vfs_virtfs_fread(void * dst, size_t size, size_t num, void * mod_data)
{
	unsigned char * to;
	unsigned char * to_tea, * to_packed, * to_unpacked;
	size_t i, j;
	size_t offset_start, offset_start_packed;
	ptrdiff_t delta_start, delta_start_packed;
	size_t offset_finish;
	size_t processed_size;
	size_t need_size, need_unpacked_size;
	size_t size_tea, size_packed;
	uint32_t * key_tea;
	size_t packtable_size;
	size_t result = 0;
	VDATA * vdata = (VDATA *)mod_data;
	if (vdata && vdata->m_rec && dst && size && num)
	{
		key_tea = vfs_virtfs_getkey(vdata->m_mod);
		if (vdata->m_filerec.m_compress_format == VIRTFS_COMPRESS_NONE)
		{
			for (i = 0; i < num; ++i)
			{
				if (size + vdata->m_offset > vdata->m_filerec.m_size)
				{
					break;
				}
				to = ((unsigned char *)dst) + size * i;
				if ((vdata->m_filerec.m_crypt_format == VIRTFS_CRYPT_TEA) && key_tea)
				{
					offset_start = vdata->m_offset / VFS_TEA_BLOCKSIZE * VFS_TEA_BLOCKSIZE;
					delta_start = vdata->m_offset - offset_start;
					offset_finish = (vdata->m_offset + size + VFS_TEA_BLOCKSIZE - 1) / VFS_TEA_BLOCKSIZE * VFS_TEA_BLOCKSIZE;
					size_tea = offset_finish - offset_start;
					if (size_tea && (to_tea = (unsigned char *)malloc(size_tea)))
					{
						vfs_fseek(vdata->m_rec->m_file, vdata->m_filerec.m_offset + offset_start, SEEK_SET);
						vfs_fread(to_tea, size_tea, 1, vdata->m_rec->m_file);
						vfs_tea_decrypt(to_tea, size_tea, key_tea);
						memcpy(to, to_tea + delta_start, size);
						free(to_tea);
					}
				} else
				if (vdata->m_filerec.m_crypt_format == VIRTFS_CRYPT_NONE)
				{
					vfs_fseek(vdata->m_rec->m_file, vdata->m_filerec.m_offset + vdata->m_offset, SEEK_SET);
					vfs_fread(to, size, 1, vdata->m_rec->m_file);
				}
				vdata->m_offset += size;
				++result;
			}
		} else
		if ((vdata->m_filerec.m_compress_format == VIRTFS_COMPRESS_LZMA) && vdata->m_packtable_recs)
		{
			packtable_size = sizeof(VIRTFS_PACKTABLE) + vdata->m_packtable.m_rec_count * sizeof(VIRTFS_PACKTABLE_RECORD);
			if (!vdata->m_pack_buff)
			{
				to_unpacked = (unsigned char *)malloc(VFS_LZMA_BLOCKSIZE);
			} else
			{
				to_unpacked = vdata->m_pack_buff;
			}
			for (i = 0; i < num; ++i)
			{
				if ((size + vdata->m_offset > vdata->m_filerec.m_size) || !to_unpacked)
				{
					break;
				}
				to = ((unsigned char *)dst) + size * i;
				if ((vdata->m_filerec.m_crypt_format == VIRTFS_CRYPT_TEA) && key_tea)
				{
					processed_size = size;
					while (processed_size)
					{
						j = vdata->m_offset / VFS_LZMA_BLOCKSIZE;
						offset_start_packed = vdata->m_packtable_recs[j].m_offset;
						size_packed = vdata->m_packtable_recs[j].m_size;
						delta_start_packed = vdata->m_offset - VFS_LZMA_BLOCKSIZE * j;
						if (((vdata->m_pack_offset != offset_start_packed) || !vdata->m_pack_buff) && size_packed && (to_packed = (unsigned char *)malloc(size_packed)))
						{
							offset_start = packtable_size + (offset_start_packed - packtable_size - vdata->m_filerec.m_offset) / VFS_TEA_BLOCKSIZE * VFS_TEA_BLOCKSIZE;
							delta_start = packtable_size + (offset_start_packed - packtable_size - vdata->m_filerec.m_offset) - offset_start;
							offset_finish = packtable_size + (offset_start_packed - packtable_size - vdata->m_filerec.m_offset + size_packed + VFS_TEA_BLOCKSIZE - 1) / VFS_TEA_BLOCKSIZE * VFS_TEA_BLOCKSIZE;
							size_tea = offset_finish - offset_start;
							if (size_tea && (to_tea = (unsigned char *)malloc(size_tea)))
							{
								need_unpacked_size = vdata->m_filerec.m_size - VFS_LZMA_BLOCKSIZE * j;
								vfs_fseek(vdata->m_rec->m_file, vdata->m_filerec.m_offset + offset_start, SEEK_SET);
								vfs_fread(to_tea, size_tea, 1, vdata->m_rec->m_file);
								vfs_tea_decrypt(to_tea, size_tea, key_tea);
								memcpy(to_packed, to_tea + delta_start, size_packed);
								vfs_lzma_unpack(to_unpacked, need_unpacked_size, to_packed, size_packed);
								if (vdata->m_pack_buff)
								{
									vdata->m_pack_offset = offset_start_packed;
								}
								free(to_tea);
							}
							free(to_packed);
						}
						need_size = (processed_size <= VFS_LZMA_BLOCKSIZE - delta_start_packed) ? processed_size : (VFS_LZMA_BLOCKSIZE - delta_start_packed);
						memcpy(to, to_unpacked + delta_start_packed, need_size);
						processed_size -= need_size;
						vdata->m_offset += need_size;
					}
				} else
				if ((vdata->m_filerec.m_crypt_format == VIRTFS_CRYPT_NONE))
				{
					processed_size = size;
					while (processed_size)
					{
						j = vdata->m_offset / VFS_LZMA_BLOCKSIZE;
						offset_start_packed = vdata->m_packtable_recs[j].m_offset;
						size_packed = vdata->m_packtable_recs[j].m_size;
						delta_start_packed = vdata->m_offset - VFS_LZMA_BLOCKSIZE * j;
						if (((vdata->m_pack_offset != offset_start_packed) || !vdata->m_pack_buff) && size_packed && (to_packed = (unsigned char *)malloc(size_packed)))
						{
							need_unpacked_size = vdata->m_filerec.m_size - VFS_LZMA_BLOCKSIZE * j;
							vfs_fseek(vdata->m_rec->m_file, offset_start_packed, SEEK_SET);
							vfs_fread(to_packed, size_packed, 1, vdata->m_rec->m_file);
							vfs_lzma_unpack(to_unpacked, need_unpacked_size, to_packed, size_packed);
							if (vdata->m_pack_buff)
							{
								vdata->m_pack_offset = offset_start_packed;
							}
							free(to_packed);
						}
						need_size = (processed_size <= VFS_LZMA_BLOCKSIZE - delta_start_packed) ? processed_size : (VFS_LZMA_BLOCKSIZE - delta_start_packed);
						memcpy(to, to_unpacked + delta_start_packed, need_size);
						processed_size -= need_size;
						vdata->m_offset += need_size;
					}
				}
				++result;
			}
			if (!vdata->m_pack_buff && to_unpacked)
			{
				free(to_unpacked);
			}
		}
	}
	return result;
}

int vfs_virtfs_fseek(void * mod_data, long int offset, int origin)
{
	VDATA * vdata = (VDATA *)mod_data;
	long int pos = 0;
	if (vdata)
	{
		switch (origin)
		{
			case SEEK_SET:
				pos = vdata->m_filerec.m_offset + offset;
				if ((vdata->m_filerec.m_offset <= pos) || (vdata->m_filerec.m_offset + vdata->m_filerec.m_size >= pos))
				{
					vdata->m_offset = offset;
					return 0;
				}
				break;
			case SEEK_CUR:
				pos = vdata->m_offset + offset;
				if ((vdata->m_filerec.m_offset <= pos) || (vdata->m_filerec.m_offset + vdata->m_filerec.m_size >= pos))
				{
					vdata->m_offset += offset;
					return 0;
				}
				break;
			case SEEK_END:
				pos = vdata->m_filerec.m_offset + vdata->m_filerec.m_size + offset;
				if ((vdata->m_filerec.m_offset <= pos) || (vdata->m_filerec.m_offset + vdata->m_filerec.m_size >= pos))
				{
					vdata->m_offset = vdata->m_filerec.m_size + offset;
					return 0;
				}
				break;
		}
	}
	return -1;
}

long int vfs_virtfs_ftell(void * mod_data)
{
	return mod_data ? ((VDATA *)mod_data)->m_offset : -1;
}
